/*
-------------------------------------------------------------------
BIE is Copyright 2001-2003 Brunswick Corp.
-------------------------------------------------------------------
Please read the legal notices (docs/legal.txt) and the license
(docs/bie_license.txt) that came with this distribution before using
this software.
-------------------------------------------------------------------
*/
package com.webdeninteractive.sbie.util;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Iterator;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import org.xml.sax.SAXException;

// Xerces imports: for dumpDoc only
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import java.io.ByteArrayOutputStream;
// XXX dumpDoc should be moved out of this class

/**
 * A collection of utility functions for manipulating DOM Documents.
 *
 * <cite><pre>
 *                                    on one side,
 *    Graven in the oldest tongue of all this world,
 *    "Take me," but turn the blade and ye shall see,
 *    And written in the speech ye speak yourself,
 *    "Cast me away!"
 * </pre></cite>
 *
 * This is intended to make creation of and extraction from simple XML 
 * documents easier than it is with the wacky DOM apis, but assumes
 * fairly uncomplicated document structure.  Also, this collection of 
 * utilities was thrown together quickly, and is a little sloppy.
 *
 * <h2>Example of Use</h2>
 *
 * <pre>
 *        Document doc = 
 *            parseInputStream( new FileInputStream( new File( args[0] ) ) );
 *        addText( doc, "chix", "Can I have a taste of your ice cream?" );
 *        addTextUnder( doc, "foo", "goo", "Thingie Number One" );
 *        addTextUnder( doc, "foo", "poo", "Stuffas Number Two" );
 *        addElementUnder( doc, "foo", "milk" );
 *        addTextUnder( doc, "milk", "bar", "Horrorshow" );
 *        addTextUnder( doc, "milk", "toast", "Soppy!" );
 *
 *        String text = getText( doc, "wobuffet" ).trim();
 *        dumpDoc(doc, System.out);
 *        System.out.println( "Wobuffet = '" + text + "'" );
 * </pre>
 *
 * @author gfast
 * @version $Id: DocumentUtils.java,v 1.1.1.1 2003/05/20 16:56:49 gdf Exp $ 
 */
public class DocumentUtils {

    /** For standalone testing. */
    public static void main( String[] args ) throws Exception {
        //Document doc = 
        //    parseInputStream( new FileInputStream( new File( args[0] ) ) );
        Document doc = makeDocument("boogers");
        addText( doc, "chix", "Can I have a taste of your ice cream?" );
        addTextUnder( doc, "foo", "goo", "Thingie Number One" );
        addTextUnder( doc, "foo", "poo", "Stuffas Number Two" );
        addElementUnder( doc, "foo", "milk" );
        addTextUnder( doc, "milk", "bar", "Horrorshow" );
        addTextUnder( doc, "milk", "toast", "Soppy!" );

        Element e = appendElement( doc, "foo" );
        e.setAttribute("name", "test1");
        addText(e, "class", "com.wdi.bie.bielite.service.Test");
        addText(e, "interval", "-1");
        addText(e, "parameter", "251").setAttribute("name", "id");
        
        String text = getText( doc, "wobuffet" ).trim();
        dumpDoc(doc, System.out);
        System.out.println( "Wobuffet = '" + text + "'" );
    }

    // as this depends on xerxes, it should be commented out for prod:
    public static void dumpDoc(Document doc, OutputStream out) {
        OutputFormat of = new OutputFormat( doc, "utf-8", true );
        of.setOmitXMLDeclaration(true);
        XMLSerializer grapenuts = new XMLSerializer( out, of );
        try {
            grapenuts.serialize(doc);
        } catch (IOException e) {
            System.out.println("***Can't serialize doc: " + e);
        }
    }

    public static String dumpDoc(Document doc) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        dumpDoc(doc, stream);
        return stream.toString();
    }


    /**
     * Helper to create a new, empty Document.
     */
    public static Document makeDocument( String rootElementName ) 
        throws ParserConfigurationException
    {
        Document newdoc = DocumentBuilderFactory.newInstance().
            newDocumentBuilder().newDocument();
        newdoc.appendChild( newdoc.createElement( rootElementName ) );
        return newdoc;
    }

    /**
     * Helper to create a new Document from an InputStream.
     */
    public static Document parseInputStream( InputStream is )
        throws ParserConfigurationException, IOException, SAXException
    {
        DocumentBuilder builder = 
            DocumentBuilderFactory.newInstance().newDocumentBuilder();
        return builder.parse( is );
    }

    /** Create a new element of the given name, and append it to the Document.
     *  If elements with this name already exist, a new one will be created.
     */
    public static Element appendElement( Document d, String name ) {
        return appendElement( d.getDocumentElement(), name );        
    }

    /** Create a new element of the given name, and append it to the
     *  given element.  If elements with this name already exist, a new
     *  one will be created. 
     */
    public static Element appendElement( Element e, String name ) {
        Element newelt = e.getOwnerDocument().createElement(name);
        e.appendChild(newelt);
        return newelt;
    }

    /** 
     * Find the first element with the given name.
     * @return the first element with the given tagname, or null if none exist.
     */
    public static Element getAnElement( Document d, String name ) {
        return getAnElement( d.getDocumentElement(), name );
    }

    /**
     * Find the first element with the given name under the given Element.
     * @return the first matching element found, or null if no matches.
     */
    public static Element getAnElement( Element e, String name ) {
        NodeList nl = e.getElementsByTagName( name );
        return (Element)nl.item(0);
    }

    /** 
     * Get the textual contents of the (first) given named element.
     * @return the text of all text nodes under the found element, or "".
     */
    public static String getText( Document d, String elementName ) {
        Element e = getAnElement(d, elementName);
        if (e!=null) {
            return getTextContent(e);
        } else {
            return "";
        }

    } // getText()

    /** Get the textual contents of the (first) element of the given
     *  name under the given element.
     *  @return the text of all text nodes under the found element, or "".
     */
    public static String getText( Element parentElt, String elementName ) {
        Element e = getAnElement(parentElt, elementName);
        if (e!=null) {
            return getTextContent(e);
        } else {
            return "";
        }

    } // getText()

    /** Returns the concatenated text nodes contained under the given element.
     *  This method does no whitespace trimming of the text nodes.
     */
    public static String getTextContentNoTrim( Element e ) {
        StringBuffer ret = new StringBuffer();
        NodeList nl = e.getChildNodes();
        for( int i=0; i<nl.getLength(); i++ ) {
            Node n = nl.item(i);
            if ( n.getNodeType() == Node.TEXT_NODE ) {
                ret.append( n.getNodeValue() );
            }
        }
        return ret.toString();

    } // getTextContentRaw()

    /** Returns the concatenated text nodes containted under the given element.
     *  The returned text will be trimmed for leading/trailing whitespace.
     */
    public static String getTextContent( Element e ) {
        String s = getTextContentNoTrim(e);
        if (s!=null) {
            s = s.trim();
        }
        return s;

    } // getTextContent()
        

    /**
     * Add some text to the Document.
     * A new text Node, containing the given String, is created under
     * the first element found whose tag matched elementName.
     * If no such element is found, one will be created under the Document
     * root.  If text Nodes already exist under elementName, the new text
     * Node will be appended (the existing contents will not be replaced).
     * @param d Document to edit.
     * @param elementName tag to add the text under, which need not yet exist.
     * @param text text to append under the found/new element.
     */
    public static Element addText( Document d,
                                   String elementName, 
                                   String text )
    {
        return addText(d.getDocumentElement(), elementName, text);

    } // addText(Document,...)

    /**
     * Add some text to a portion of a Document.
     * This method targets the first element of the given elementName
     * contained under the given rootElement.  If none is found, a new
     * element under the rootElement will be created as the target.  A
     * new text node containing the given text String will then be
     * created under the target element.
     *
     * @param rootElement Element to add text under.
     * @param elementName name of sub-element to add text contents to.
     * @param text stuff to append to targeted subelement.
     */
    public static Element addText( Element rootElement,
                                   String elementName,
                                   String text )
    {
        Element e = getAnElement(rootElement, elementName);
        if (e==null) { // create element if it doesn't exist
            e = rootElement.getOwnerDocument().createElement(elementName);
            rootElement.appendChild(e);
        }
        e.appendChild( rootElement.getOwnerDocument().createTextNode( text ) );
        return e;

    } // addText(Element,...)

    /**
     * Add some text to the Document, below a given element.
     * This is like {@link #addText}, but the element to find or create is
     * specified by a parent-child pair of tag names.  This locates the parent
     * element, and adds text in an element using the child name.
     * If either the parent or the child doesn't exist, they will be created
     * (the parent will be created under the Document root).
     * @param d Document to edit.
     * @param parentElementName tag name of parent element to locate or create.
     * @param childElementName tag name of child element to locate or create.
     * @param text text to append under child element.
     */
    public static Element addTextUnder( Document d,
                                        String parentElementName,
                                        String childElementName,
                                        String text )
    {
        Element pe = getAnElement(d, parentElementName);
        if (pe==null) { // create parent if there isn't one yet
            pe = d.createElement(parentElementName);
            d.getDocumentElement().appendChild(pe);
        }
        Element ce = getAnElement( pe, childElementName );
        if (ce==null) { //create child, too, if it's new
            ce = d.createElement(childElementName);
            pe.appendChild(ce);
        }
        ce.appendChild( d.createTextNode( text ) );
        return ce;

    } // addTextUnder()

    /**
     * Creates/finds an element under the given parent, with a given
     * name.  If either element doesn't exist, it will be created (the
     * parent will be created under the Document root).  A newly
     * created child node wil be empty.
     * @param d Document to edit.
     * @param parentElementName tag name of parent to locate or create.
     * @param childElementName tag name of child to locate or create.
     * @return the found or created Element.
     */
    public static Element addElementUnder( Document d,
                                           String parentElementName,
                                           String childElementName )
    {
        Element pe = getAnElement(d, parentElementName);
        if (pe==null) { // create parent if there isn't one yet
            pe = d.createElement(parentElementName);
            d.getDocumentElement().appendChild(pe);
        }
        Element ce = getAnElement( pe, childElementName );
        if (ce==null) { //create child, too, if it's new
            ce = d.createElement(childElementName);
            pe.appendChild(ce);
        }
        return ce;
        
    } // addElementUnder()


    /** As getElementsByTagName, but only returns direct children.
     *  getElementsByTagName is annoying, since it returns all sub-elements
     *  with the given tag name.  If one is traversing a non-strict Document
     *  where multiple elements may contain elements with the same tag name,
     *  it is useful to be able to get only immediate child matches.
     * @return Collection of Elements.
     */
    public static Collection getChildElements( Element element,
                                               String tagName )
    {
        ArrayList ret = new ArrayList();
        NodeList nl = element.getChildNodes();
        for( int i=0; i<nl.getLength(); i++ ) {
            Node n = nl.item(0);
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                Element e = (Element)n;
                if (e.getTagName().equals(tagName)) {
                    ret.add(e);
                }
            }
        }
        return ret;

    } // getChildElements()

    /** Add some text content directly to a given Element. */
    public static Element addTextContent( Element e, String text ) {
        e.appendChild( e.getOwnerDocument().createTextNode( text ) );
        return e;
    }

}

